﻿Option Strict On

Public Class BassFunctions

    Private Enum PropertyTypeEnum As Short
        ByteArray = 1
        AsciiString = 2
        ShortArray = 3
        IntArray = 4
        Any = 6
    End Enum

    Private Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" (ByVal lpstrCommand As String, ByVal lpstrReturnString As String, ByVal uReturnLength As Integer, ByVal hwndCallback As Integer) As Integer

    Public Event TracksRead(trackCount As Integer)
    Public Event TrackIncrement()

    Public Property Filename As String
    Public Property WaveformData As Misc.WaveForm
    Public Property Markers As List(Of Double)

    Public Sub Save(filename As String)
        Dim s As IO.Stream
        Dim wr As IO.BinaryWriter
        Dim ser As Runtime.Serialization.Formatters.Binary.BinaryFormatter
        Dim bytes() As Byte

        bytes = Me.WaveformData.WaveFormSaveToMemory()

        If bytes IsNot Nothing Then
            s = IO.File.OpenWrite(filename)
            wr = New IO.BinaryWriter(s)

            ' write waveform data
            wr.Write(bytes.Length)
            wr.Write(bytes)

            If Markers IsNot Nothing Then
                Using ms As New IO.MemoryStream
                    ser = New Runtime.Serialization.Formatters.Binary.BinaryFormatter()
                    ser.Serialize(ms, Me.Markers)

                    bytes = ms.ToArray

                End Using

            End If

            ' write marker data
            With wr
                .Write(bytes.Length)
                .Write(bytes)
                .Close()
                .Dispose()
            End With

            s.Close()
            s.Dispose()

        End If

    End Sub

    Shared Function Load(filename As String) As BassFunctions
        Dim output As New BassFunctions
        Dim s As IO.Stream
        Dim br As IO.BinaryReader
        Dim ser As Runtime.Serialization.Formatters.Binary.BinaryFormatter
        Dim bytes() As Byte
        Dim length As Integer

        s = IO.File.OpenRead(filename)
        If s.Length > 0 Then
            br = New IO.BinaryReader(s)

            ' read waveform data
            length = br.ReadInt32
            bytes = br.ReadBytes(length)

            output.WaveformData = New Misc.WaveForm
            output.WaveformData.WaveFormLoadFromMemory(bytes)

            ' read marker data
            length = br.ReadInt32
            bytes = br.ReadBytes(length)

            ser = New Runtime.Serialization.Formatters.Binary.BinaryFormatter
            output.Markers = TryCast(ser.Deserialize(New IO.MemoryStream(bytes)), List(Of Double))
        End If

        s.Close()

        Return output

    End Function

    Public Sub EjectCD(drive As String)
        Dim ret As Integer
        Dim retString As String = ""

        ret = mciSendString($"open ""{drive}"" type cdaudio alias cd wait shareable", retString, 0, 0)
        ret = mciSendString("set cd door open", retString, 0, 0)

    End Sub

    Public Sub ReadTracksFromCD(driveNumber As Integer, sequence As List(Of Integer))
        Dim inputHandle As Integer
        Dim encHandle As Integer
        Dim outputHandle As Integer
        Dim bytes() As Byte
        Dim length As Long
        Dim elapsed As Double

        Me.Markers = New List(Of Double)

        ' Set up sequence of tracks
        If sequence Is Nothing OrElse sequence.Count = 0 Then
            ' load all tracks in sequence
            sequence = New List(Of Integer)

            Debug.WriteLine("Read Tracks")
            For i As Integer = 1 To AddOn.Cd.BassCd.BASS_CD_GetTracks(driveNumber)
                sequence.Add(i)
            Next

        End If

        ' inform UI of number of tracks
        RaiseEvent TracksRead(sequence.Count)
        Application.DoEvents()

        Debug.WriteLine("Open output stream")
        encHandle = AddOn.Mix.BassMix.BASS_Mixer_StreamCreate(44100, 2, BASSFlag.BASS_STREAM_DECODE)
        outputHandle = AddOn.Enc.BassEnc.BASS_Encode_Start(encHandle, Me.Filename & ".wav", AddOn.Enc.BASSEncode.BASS_ENCODE_PCM, Nothing, IntPtr.Zero)

        For Each i As Integer In sequence
            Debug.WriteLine($"Read track {i}")
            inputHandle = AddOn.Cd.BassCd.BASS_CD_StreamCreate(driveNumber, i - 1, BASSFlag.BASS_STREAM_DECODE)

            Debug.WriteLine($"Get length of track {i}")
            length = Bass.BASS_ChannelGetLength(inputHandle)
            elapsed += Bass.BASS_ChannelBytes2Seconds(inputHandle, length)

            AddOn.Mix.BassMix.BASS_Mixer_StreamAddChannel(encHandle, inputHandle, BASSFlag.BASS_STREAM_DECODE)

            Debug.WriteLine("Start playing track")
            Bass.BASS_ChannelPlay(inputHandle, False)

            If Bass.BASS_ChannelIsActive(encHandle) = BASSActive.BASS_ACTIVE_PLAYING Then
                ReDim bytes(CInt(length - 1))

                Debug.WriteLine($"Read {length} bytes from track {i}")
                Bass.BASS_ChannelGetData(inputHandle, bytes, bytes.Length)

                Debug.WriteLine("Write bytes to output stream")
                AddOn.Enc.BassEnc.BASS_Encode_Write(outputHandle, bytes, bytes.Length)

                Markers.Add(elapsed)

            End If

            Bass.BASS_StreamFree(inputHandle)

            ' inform UI of track advance
            RaiseEvent TrackIncrement()

            Application.DoEvents()

        Next

        Debug.WriteLine("Close output stream")
        AddOn.Enc.BassEnc.BASS_Encode_Stop(outputHandle)
        Bass.BASS_StreamFree(encHandle)
        Bass.BASS_StreamFree(outputHandle)

    End Sub

    Public Sub ReadTracksFromFolder(folderName As String)
        Dim files As String()
        Dim inputHandle As Integer
        Dim encHandle As Integer
        Dim outputHandle As Integer
        Dim bytes() As Byte
        Dim length As Long
        Dim elapsed As Double

        Me.Markers = New List(Of Double)

        Debug.WriteLine("Read Tracks")
        files = IO.Directory.GetFiles(folderName)
        Array.Sort(files)

        RaiseEvent TracksRead(files.Count)

        Debug.WriteLine("Open output stream")
        encHandle = AddOn.Mix.BassMix.BASS_Mixer_StreamCreate(44100, 2, BASSFlag.BASS_STREAM_DECODE)
        outputHandle = AddOn.Enc.BassEnc.BASS_Encode_Start(encHandle, Me.Filename & ".wav", AddOn.Enc.BASSEncode.BASS_ENCODE_PCM, Nothing, IntPtr.Zero)

        For i As Integer = 0 To files.Count - 1
            Try
                Debug.WriteLine($"Read track {i + 1}")

                Select Case True
                    Case files(i).EndsWith(".wma", StringComparison.CurrentCultureIgnoreCase)
                        inputHandle = AddOn.Wma.BassWma.BASS_WMA_StreamCreateFile(files(i), 0, -1, BASSFlag.BASS_STREAM_DECODE)

                    Case files(i).EndsWith(".flac", StringComparison.CurrentCultureIgnoreCase)
                        inputHandle = AddOn.Flac.BassFlac.BASS_FLAC_StreamCreateFile(files(i), 0, -1, BASSFlag.BASS_STREAM_DECODE)

                    Case files(i).EndsWith(".wav", StringComparison.CurrentCultureIgnoreCase)
                        inputHandle = Bass.BASS_StreamCreateFile(files(i), 0, -1, BASSFlag.BASS_STREAM_DECODE)

                    Case files(i).EndsWith(".mp3", StringComparison.CurrentCultureIgnoreCase)
                        inputHandle = Bass.BASS_StreamCreateFile(files(i), 0, -1, BASSFlag.BASS_STREAM_DECODE)

                End Select

                If inputHandle = 0 Then
                    Debug.WriteLine($"Skipping {files(i)} due to unsupported format")
                    Continue For
                End If

                Debug.WriteLine($"Get length of track {i + 1}")
                length = Bass.BASS_ChannelGetLength(inputHandle)
                elapsed += Bass.BASS_ChannelBytes2Seconds(inputHandle, length)

                AddOn.Mix.BassMix.BASS_Mixer_StreamAddChannel(encHandle, inputHandle, BASSFlag.BASS_STREAM_DECODE)

                Debug.WriteLine("Start playing track")
                Bass.BASS_ChannelPlay(inputHandle, False)

                If Bass.BASS_ChannelIsActive(encHandle) = BASSActive.BASS_ACTIVE_PLAYING Then
                    ReDim bytes(CInt(length - 1))

                    Debug.WriteLine($"Read {length} bytes from track {i + 1}")
                    Bass.BASS_ChannelGetData(inputHandle, bytes, bytes.Length)

                    Debug.WriteLine("Write bytes to output stream")
                    AddOn.Enc.BassEnc.BASS_Encode_Write(outputHandle, bytes, bytes.Length)

                    Me.Markers.Add(elapsed)

                End If

                RaiseEvent TrackIncrement()
                Application.DoEvents()

            Finally
                Bass.BASS_StreamFree(inputHandle)
                inputHandle = 0

            End Try

        Next

        Debug.WriteLine("Close output stream")
        AddOn.Enc.BassEnc.BASS_Encode_Stop(outputHandle)
        Bass.BASS_StreamFree(encHandle)
        Bass.BASS_StreamFree(outputHandle)

    End Sub

    Public Function Peak() As Double
        Dim max As Short = 0

        For Each d As Misc.WaveForm.WaveBuffer.Level In Me.WaveformData.Wave.data
            If d.left = Short.MinValue Then
                If Short.MaxValue > max Then max = Short.MaxValue
            Else
                If Math.Abs(d.left) > max Then max = Math.Abs(d.left)

            End If

            If d.right = Short.MinValue Then
                If Short.MaxValue > max Then max = Short.MaxValue
            Else
                If Math.Abs(d.right) > max Then max = Math.Abs(d.right)

            End If

            ' If we hit maximum, no sense in wasting any further time
            If max = Short.MaxValue Then Exit For

        Next

        Return Math.Round((max / Short.MaxValue) * 100, 2)

    End Function

    Public Function Average() As Double
        Dim avg As Double = 0
        Dim d() As Misc.WaveForm.WaveBuffer.Level

        d = Me.WaveformData.Wave.data

        For n = 0 To Me.WaveformData.Wave.data.Length - 1
            If d(n).left = Short.MinValue Then
                avg = (avg * n + Short.MaxValue) / (n + 1)
            Else
                avg = (avg * n + Math.Abs(Me.WaveformData.Wave.data(n).left)) / (n + 1)
            End If

            If d(n).right = Short.MinValue Then
                avg = (avg * n + Short.MaxValue) / (n + 1)
            Else
                avg = (avg * n + Math.Abs(Me.WaveformData.Wave.data(n).right)) / (n + 1)
            End If

        Next

        Return Math.Round((avg / Short.MaxValue) * 100, 2)

    End Function

    Shared Sub CreateWaveformImage(filename() As String, saveFilename As String, Optional gain() As Single = Nothing, Optional stretch() As Single = Nothing, Optional offset() As Integer = Nothing)
        Dim data As BassFunctions
        Dim bmpFrame As Bitmap
        Dim bmpFinal As Bitmap
        Dim gr As Graphics
        Dim JPEGCodec As Imaging.ImageCodecInfo
        Dim params As Imaging.EncoderParameters
        Dim colors As List(Of Color)
        Dim caption As String
        Dim leftMargin As Integer
        Dim peaks As New List(Of Double)
        Dim comments As New System.Text.StringBuilder
        Dim prop As Imaging.PropertyItem
        Dim propBytes() As Byte
        Dim maxOffset As Integer

        If offset IsNot Nothing Then
            maxOffset = offset.Max
        Else
            maxOffset = 0
        End If

        ' make list of colors. opacity depends on number of files (1=100%, 2=50%, 3=33%, 4=25%)
        colors = New List(Of Color)
        With colors
            .Add(Color.FromArgb(255 \ filename.Length, My.Settings.WaveformColor1))
            .Add(Color.FromArgb(255 \ filename.Length, My.Settings.WaveformColor2))
            .Add(Color.FromArgb(255 \ filename.Length, My.Settings.WaveformColor3))
            .Add(Color.FromArgb(255 \ filename.Length, My.Settings.WaveformColor4))
        End With

        bmpFinal = New Bitmap(My.Settings.WaveformSize.Width, My.Settings.WaveformSize.Height + 30)
        gr = Graphics.FromImage(bmpFinal)
        ' draw background
        gr.FillRectangle(Brushes.Black, New Rectangle(0, 0, bmpFinal.Width, bmpFinal.Height))

        comments.Append("Files: ")

        ' generate waveform image for each file
        For i As Integer = 0 To filename.Count - 1
            comments.Append(filename(i) & ", ")
            data = BassFunctions.Load(filename(i))

            With data.WaveformData
                .ColorBackground = Color.Transparent
                .ColorLeft = colors(i)
                .ColorRight = colors(i)
                .ColorLeftEnvelope = colors(i)
                .ColorRightEnvelope = colors(i)
                .ColorMarker = Color.White
                .WaveFormLoadFromFile(filename(i))

                If My.Settings.DrawTrackBoundaries Then
                    .DrawMarker = Misc.WaveForm.MARKERDRAWTYPE.Line
                    For m As Integer = 0 To data.Markers.Count - 1
                        .AddMarker($"Pos {m}", data.Markers(m))
                    Next
                End If
                .GainFactor = If(IsNothing(gain), 1, gain(i))
                .RenderStart(False, BASSFlag.BASS_SAMPLE_FLOAT Or BASSFlag.BASS_STREAM_PRESCAN)
            End With

            bmpFrame = data.WaveformData.CreateBitmap(My.Settings.WaveformSize.Width - maxOffset + If(IsNothing(offset), 0, offset(i)), My.Settings.WaveformSize.Height, 0, data.WaveformData.FramesRendered, True)
            gr.DrawImage(bmpFrame, New Point(If(IsNothing(offset), 0, offset(i)), 0))
            bmpFrame.Dispose()

            peaks.Add(data.Peak)

        Next

        ' trim off trailing comma
        comments.Length -= 2

        If gain IsNot Nothing Then
            comments.AppendLine("")
            comments.Append("Gain Adj: ")
            For Each item In gain
                comments.Append(item & ", ")
            Next
            comments.Length -= 2

        End If

        If offset IsNot Nothing Then
            comments.AppendLine("")
            comments.Append("Offsets: ")
            For Each item In offset
                comments.Append(item & ", ")
            Next
            comments.Length -= 2

        End If

        With gr
            ' Draw center line between waveforms
            .DrawLine(New Pen(Color.FromArgb(98, 108, 123), 2), New Point(0, My.Settings.WaveformSize.Height \ 2), New Point(bmpFinal.Width, My.Settings.WaveformSize.Height \ 2))
            ' Draw border
            .DrawRectangle(New Pen(Color.FromArgb(98, 108, 123), 2), New Rectangle(New Point(1, 1), New Size(bmpFinal.Width - 2, bmpFinal.Height - 2)))

            ' Add legend for each file
            leftMargin = 5
            For i As Integer = 0 To filename.Count - 1
                .FillRectangle(New SolidBrush(Color.FromArgb(255, colors(i))), New Rectangle(New Point(leftMargin, My.Settings.WaveformSize.Height + 15), New Size(10, 8)))
                leftMargin += 12

                caption = $"{IO.Path.GetFileNameWithoutExtension(filename(i))} (Peak: {peaks(i)}%)"
                .DrawString(caption, New Font(FontFamily.GenericSansSerif, 9), Brushes.White, New Point(leftMargin, My.Settings.WaveformSize.Height + 10))
                leftMargin += CInt(.MeasureString(caption, New Font(FontFamily.GenericSansSerif, 9)).Width)
                leftMargin += 5
            Next

            .Dispose()

        End With

        propBytes = System.Text.ASCIIEncoding.Default.GetBytes("CDWaveform" & vbNullChar)
        prop = CType(System.Runtime.Serialization.FormatterServices.GetUninitializedObject(GetType(Imaging.PropertyItem)), Imaging.PropertyItem)
        With prop
            .Id = 305 ' Software
            .Type = PropertyTypeEnum.AsciiString
            .Len = propBytes.Length
            .Value = propBytes
        End With
        bmpFinal.SetPropertyItem(prop)

        propBytes = System.Text.ASCIIEncoding.Default.GetBytes(comments.ToString & vbNullChar)
        prop = CType(System.Runtime.Serialization.FormatterServices.GetUninitializedObject(GetType(Imaging.PropertyItem)), Imaging.PropertyItem)
        With prop
            .Id = 37510 ' User Comment
            .Type = PropertyTypeEnum.AsciiString
            .Len = propBytes.Length
            .Value = propBytes
        End With
        bmpFinal.SetPropertyItem(prop)

        ' get the JPEG codec
        JPEGCodec = Imaging.ImageCodecInfo.GetImageEncoders.First(Function(x) x.MimeType = "image/jpeg")
        params = New Imaging.EncoderParameters
        params.Param(0) = New Imaging.EncoderParameter(Imaging.Encoder.Quality, 95)


        bmpFinal.Save(saveFilename, JPEGCodec, params)

    End Sub

End Class
